浏览器功能定制3:使用 mitmproxy 增强浏览器的拦截能力

在《浏览器功能定制2:显示网页正文的阅读模式》中,我实现了网页的正文提取。于是继续着手于《浏览器功能定制1:将网页内容保存为 Markdown》,但是我遇到了新的挑战:如何将网页中的图片离线化。

Qt QWebEngine,你可以很方便地打开 devtools,查看网络请求、资源。同时,你也可以很方便地通过代码添加拦截器,拦截住网页的每一个请求。

但是,你没法通过代码,拦截到每一个请求的响应。(如果有方法,请告诉我)。

在查阅一番资料都没有找到答案后,我准备这么搞:

既然浏览器内部没有提供方便的数据拦截机制,那我就从外部找实现方案——mitmproxy。给浏览器加一层网络拦截层。

这是一个比较猛的方案,其功能远远不止图片离线化。更多强大功能,有待于慢慢发掘。在本文中,还是以图片离线化,作为切入场景。

mitmproxy 介绍

mitmproxy 是一个由 Python 开发的类似于 Charles 的网络请求调试工具。可以作为命令行工具运行。

但是,它还可以作为 Python ,嵌入到程序中运行。我们正是利用了这个神奇特性。

需要注意,尽管是在程序内使用 mitmproxy,但操作系统还是要安装 mitmproxy 的证书。参见《mitmproxy 安装证书》。

多进程

由于 qutebrowser 运行在主进程上,我希望将 mitmproxy 运行在单独进程中,相互隔离。

具体代码如下:

import asyncio
from mitmproxy.tools.dump import DumpMaster
from mitmproxy.options import Options as ProxyOptions
from mitmproxy.addons import TermLog, DumpAddon
from multiprocessing import Process

# 定义一个异步函数来运行服务器
async def run_proxy():
    opts = ProxyOptions(listen_host='127.0.0.1', listen_port=8080)
    m = DumpMaster(opts)
    m.addons.add(TermLog())
    m.addons.add(RequestLogger())
    await m.run()

# 定义一个函数来启动服务器的事件循环
def start_proxy():
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    loop.run_until_complete(run_proxy())

# 创建一个Process对象并启动它
proxy_process = Process(target=start_proxy)
proxy_process.start()

# 这里可以初始化和启动你的PyQt应用程序
# ...

# 在应用程序退出后确保代理服务器进程也关闭
proxy_process.join()

其中:

拦截器

拦截器的代码实现如下:

class RequestLogger:
    images_map = {}

    def response(self, flow: http.HTTPFlow):
        response = flow.response
        if response == None:
            return
        if response.content == None:
            return

        headers = dict(response.headers)
        content_type = headers.get("content-type", "")

        # save images to images_map
        if content_type.startswith("image/"):
            path = urlparse(flow.request.url).path
            filename = os.path.basename(path)
            if filename:
                print(f'save image: {filename}')
                self.images_map[filename] = response.content
            else:
                print('error image url: {flow.request.url}')

这里给出了最核心的实现。

qutebrowser 中启动多线程

与 QApplication 一同启动时,需要注意技巧,如下代码示意:

if __name__ == '__main__':
    # 初始化 PyQt 应用
    app = QApplication(sys.argv)
    
    # 在 PyQt 应用程序之前启动 mitmproxy 进程
    proxy_process = Process(target=start_proxy)
    proxy_process.start()

    # 初始化和启动你的 PyQt 应用程序
    window = YourMainWindow()
    window.show()

    # 启动事件循环
    exit_code = app.exec_()  # 注意:这里是 exec_() 而不是 exec(),这是 PyQt 的特性

    # 等待代理进程结束
    proxy_process.join()

    sys.exit(exit_code)

qutebrowser 设置

可以通过如下命令设置:

:set content.proxy http://localhost:11270

小结

至此,qutebrowser 的所有流量都将经过 mitmproxy 传输。而我们可以通过代码控制 mitmproxy,从而掌控流量出入的每一个细节。

这带来了无数可能,令人激动。


本文作者:Maeiee

本文链接:浏览器功能定制3:使用 mitmproxy 增强浏览器的拦截能力

版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!


喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!